从炫酷的波浪动画学习anime.js设计原理 您所在的位置:网站首页 anime style滤镜怎么用 从炫酷的波浪动画学习anime.js设计原理

从炫酷的波浪动画学习anime.js设计原理

2024-02-20 08:17| 来源: 网络整理| 查看: 265

anime.js

Anime.js 是一个Star数44k的轻量级JavaScript动画库,提供了简单而灵活的API,支持创建所有类型的动画,包括关键帧动画、CSS 动画和 SVG 动画以及Javascript对象。 Anime.js具有清晰简洁的语法,可以轻松快速地创建出复杂动画。支持的动画功能包括缓动、时间轴、回调等,使其成为一个多功能的动画工具,可以为从简单的页面转换到复杂的UI元素的所有内容制作动画。

anime({ targets: 'div', translateX: 250, rotate: '1turn', backgroundColor: '#FFF', duration: 800 });

Anime.js的一个关键特性是文件很小,非常适合在移动应用程序和其他性能至关重要的环境中使用。非常多的社区开发人员为其开发和改进做出了贡献, 因此也到秩序维护和更新。

学习优秀的开源库是了解底层原理的重要手段,anime.js的star数接近45k,也证明了它的优秀。相比其他动画库,anime.js提供stagger、timeline、control模块为动画执行提供了高灵活性、扩展性。

动画拆解

4月-18-2023 09-19-41.gif 打开anime.js官网,首页即可看到上图的波浪效果,如何从0到实现一个类似的动画效果?除了赞叹炫酷之外,只剩一脸懵逼,不知如何下手。一个波浪动画的实现,大致可分为以下几个步骤:

初始化元素和样式: 元素由32 * 16的矩阵组成,因此需要将这些元素按顺序排列并设置初始样式。 动画参数设置: 设置动画执行效果、执行周期等属性。 动画执行顺序:动画由某个随机位置(如位置[15, 8])向外蔓延,由近到远延迟执行。 多关键帧实现: 一个复杂动画一般由多个关键帧组合构成。

接下来就按这几个步骤依次介绍anime.js的底层实现。

初始化元素和样式

.dots-wrapper节点作为元素排列的容器,水平、垂直居中显示在屏幕上。.dot指每个元素的样式,由于容器大小为32 * 16, 一共包含32*16个元素按[32, 16]矩阵排列。

:root { font-size: 20px; } body { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; height: 100vh; background-color: #000; } .stagger-visualizer { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; position: relative; width: 32rem; height: 16rem; } .dots-wrapper { position: absolute; display: flex; flex-wrap: wrap; justify-content: center; align-items: center; width: 100%; height: 100%; } .stagger-visualizer .dot { position: relative; width: calc(1rem - 8px); height: calc(1rem - 8px); margin: 4px; border-radius: 50%; background-color: currentColor; }

设置矩阵大小为[32, 16],那么元素总数numberOfElements即为32*16,直接创建numberOfElements个dom元素并设置样式为dot。

var staggerVisualizerEl = document.querySelector('.stagger-visualizer'); var dotsWrapperEl = staggerVisualizerEl.querySelector('.dots-wrapper'); var dotsFragment = document.createDocumentFragment(); var grid = [32, 16]; var cellSize = 1; var numberOfElements = grid[0] * grid[1]; var animation; var paused = true; for (var i = 0; i < numberOfElements; i++) { var dotEl = document.createElement('div'); dotEl.classList.add('dot'); dotsFragment.appendChild(dotEl); } dotsWrapperEl.appendChild(dotsFragment);

屏幕快照 2023-04-19 下午11.04.27.png

参数设置

有了元素素材,接下来考虑让元素动起来。在实例化动画时,通过配置基础参数,确定动画的执行动作和顺序。几个关键参数:

keyFrames:类似CSS的关键帧,区别在于CSS关键帧按百分比分布,如0%、20%、...、100%分多个阶段,而这里的keyFrames定义为数组类型,后面单独介绍。 delay: 当动画的目标元素包含多个时,常常需要元素与元素间有延迟执行,如排列的32 * 16个元素,当从中间触发动画时,中间元素先执行、四周元素后执行。delay可以定义为函数类型,支持动态设置元素样式。 easing: 执行动画执行效果,包含淡入easIn、淡出easOut、淡入淡出三次贝塞尔easeInOutCubic。 complete: 假如当前动画结束后,需要自动执行下一次动画,complete指定上一次动画执行完成之后需要触发的回调。 function play() { ... animation = anime({ targets: '.stagger-visualizer .dot', keyframes: [ { zIndex: function(el, i, total) { return Math.round(anime.stagger([numberOfElements, 0], {grid: grid, from: index})(el, i, total)); }, translateX: anime.stagger('-.001rem', {grid: grid, from: index, axis: 'x'}), translateY: anime.stagger('-.001rem', {grid: grid, from: index, axis: 'y'}), duration: 200 }, { translateX: anime.stagger('.075rem', {grid: grid, from: index, axis: 'x'}), translateY: anime.stagger('.075rem', {grid: grid, from: index, axis: 'y'}), scale: anime.stagger([2, .2], {grid: grid, from: index}), backgroundColor: staggeredGridColors, duration: 450 }, { translateX: 0, translateY: 0, scale: 1, duration: 500, } ], delay: anime.stagger(60, {grid: grid, from: index}), easing: 'easeInOutQuad', complete: play }) ... } play();

以上代码初始化了动画执行需要的参数

targets: 指定动画要素,也就是所有class包含.dot的元素; complete: 设置当动画执行完成后的回调,这里当上一个动画执行完成后重复执行play函数,只是每次指定的初始位置index不同,设置为随机索引index = anime.random(0, numberOfElements-1); delay: 指定元素与元素之间的延迟间隔,类型可以为number或函数。函数可以为(el, i) => i * 150,表示元素间动画延迟间隔为150ms。 函数还可设置为更高级的anime.stagger(150),stagger后面再详细介绍; easing: 指定缓冲效果,这里设置为三次贝塞尔easeInOutQuad效果; keyframes:数组类型,每一项设置元素属性值,如设置translateX、translateY、scale属性,指定周期duration等。属性值也可通过stagger动态调整,这里能看出stagger是一个非常重要的函数;

这几个参数的底层逻辑了解清楚后,anime.js的设计原理掌握也就八九不离十。

事件机制 anime({ ... complete: play })

类似于React的生命周期机制,anime在动画执行环节提供了begin、change、complete多种生命周期事件,并在process进度更新过程触发相应事件,在初始化通过传入以下事件就能在外部实现监听和扩展。

// 设置动画hook事件、循环次数、方向等参数 const defaultInstanceSettings = { update: null, // 每一帧动画都会触发update begin: null, // 动画开始时触发,只执行一次 loopBegin: null, // 当启动循环loop执行时,每一次循环开始都会触发loopBegin changeBegin: null, change: null, changeComplete: null, loopComplete: null, // 一次循环结束 complete: null, // 动画结束 loop: 1, // 动画循环次数,如果为true表示无限循环 direction: 'normal', // 动画执行方向,包含normal、revserse、alternate autoplay: true, // 是否自动播放 timelineOffset: 0 // } 事件周期

在了解上面的各项事件之前,先介绍duration、delay、endDelay三个和时间相关的参数。 image.png 动画开始的时刻等同于上一个动画结束的时刻,如果没有上一个动画则开始时刻为0.

tween.start = previousTween ? previousTween.end : 0;

动画结束的时刻需要加上delay、duration、enddelay三个时间段。

tween.end = tween.start + tween.delay + tween.duration + tween.endDelay;

所以一个动画实际的运行周期为tween.end - tween.start,而动画真正的执行时间仅在duration阶段。

anime.js提供了update、begin、loopBegin、changeBegin、change、changeComplete、loopComplete、complete事件,这些事件可以分为两类,一类是按帧触发(如update、change),另一类按时间片刻触发。

按帧触发:update、change的区别在于,update在start到end期间每一帧触发,而change仅在动画执行duration期间按帧触发。 按片刻触发:执行的顺序begin->loopBegin->changeBegin->changeComplete->loopComplete->complete。

image.png 在整个动画执行期间,begin、complete只会执行一次,当设置了loop(如loop: 3)则loopBegin到loopComplete之间的事件会执行多次。

事件执行

anime动画实体提供有tick函数,通过requestAnimationFrame按帧执行。其中speed默认为1,lastTime默认为0, 因此setInstanceProgress传入的参数为已执行的时间差。

instance.tick = function(t) { now = t; if (!startTime) startTime = now; setInstanceProgress((now + (lastTime - startTime)) * anime.speed); }

setInstanceProgress函数包含了动画执行过程的所有事件,函数整体分时间计算、启动事件、change事件、结束事件四部分。

时间计算: insDuration动画执行周期、insDelay动画执行延期周期,insTime为已执行时间。adjustTime函数会判断动画的reversed是否反转属性,revsersed为true时动画反转同时进度也会从100%到0%。 function setInstanceProgress(engineTime) { // 动画执行周期 const insDuration = instance.duration; // 动画延迟执行时间 const insDelay = instance.delay; const insEndDelay = insDuration - instance.endDelay; // 已执行时间 const insTime = adjustTime(engineTime); instance.progress = minMax((insTime / insDuration) * 100, 0, 100); instance.reversePlayback = insTime < instance.currentTime; ... } 启动事件: instance.began标记动画是否启动,如果第一次启动则触发begin事件。instance.loopBegan标记一次循环开始,同时触发loopBegin事件。接下来调用setAnimationsProgress函数设置两种边界的进度值,如果执行时间小于insDelay则设置进度为0,如果执行时间已经大于insEndDelay则设置进度为最大值。 function setInstanceProgress(engineTime) { ... // 触发begin hook if (!instance.began && instance.currentTime > 0) { instance.began = true; setCallback('begin'); } // 触发loopBegin,循环启动 if (!instance.loopBegan && instance.currentTime > 0) { instance.loopBegan = true; setCallback('loopBegin'); } // 如果执行时间小于insDelay,设置为0 if (insTime = insEndDelay && instance.currentTime !== insDuration) || !insDuration) { setAnimationsProgress(insDuration); } ... } Change事件: 当执行时间处于insDelay和insEndDelay之间时,第一次会触发changeBegin,但每一帧都会触发change事件。否则,表示本轮动画执行结束并触发changeComplete事件。 function setInstanceProgress(engineTime) { ... // 当前时间大于delay并且小于endDelay,表示处于可执行时间 if (insTime > insDelay && insTime < insEndDelay) { // 第一帧执行,触发changeBegin if (!instance.changeBegan) { instance.changeBegan = true; instance.changeCompleted = false; setCallback('changeBegin'); } // 每一帧执行都会触发change hook setCallback('change'); setAnimationsProgress(insTime); } else { // 如果changeBegan已触发,并且执行时间已超出,则触发changeComplete hook if (instance.changeBegan) { instance.changeCompleted = true; instance.changeBegan = false; setCallback('changeComplete'); } } ... } 事件结束: 通过事件周期可知不管动画处于哪个环节,每一帧渲染都会触发update事件。当engineTime >= insDuration,表明本轮动画执行结束,接下来就该执行结束相关的事件,先重新统计动画迭代次数,然后使用remaining判断是否还剩余迭代次数。 remaining为false,表明迭代次数执行完毕,loopComplete、complete事件被触发。 ramaining为true,表明迭代次数还未执行完,那么仅触发loopComplete事件。 function setInstanceProgress(engineTime) { ... if (instance.began) setCallback('update'); // 如果engineTime时间大于执行周期,表示本次循环执行结束 if (engineTime >= insDuration) { lastTime = 0; // 假如loop为数字,每执行一次需要更新剩余迭代次数 countIteration(); if (!instance.remaining) { instance.paused = true; if (!instance.completed) { instance.completed = true; // 如果循环执行完毕,触发loopComplete和complete两个hook setCallback('loopComplete'); setCallback('complete'); if (!instance.passThrough && 'Promise' in window) { // 当动画执行结束时,执行promise的resolve,外部可通过instance.finished.then添加后置函数 resolve(); promise = makePromise(instance); } } } else { // 如果循环次数还未结束,则仅触发loopComplete startTime = now; setCallback('loopComplete'); instance.loopBegan = false; // 如果direction为交替执行,则需要更新reversed状态记录当前执行的方向 if (instance.direction === 'alternate') { toggleInstanceDirection(); } } } ... }

到此,anime.js涉及的所有事件全部介绍完毕。

delay

在初始化动画时,可设置延迟执行参数delay,除了为数字类型(如1000),anime.js还支持stagger类型。stagger字面意思交错、错开,结合下面的代码看,targets为组成波浪矩阵的所有元素,这些元素按什么顺序执行?

animation = anime({ targets: '.stagger-visualizer .dot', ... delay: anime.stagger(60, {grid: grid, from: index}), easing: 'easeInOutQuad', complete: play }, 30)

delay设置为anime.stagger(60, {grid: grid, from: index}), 第一个参数指定元素与元素之间延迟间隔为60ms,第二个参数设定元素的执行顺序按网格[32, 16]中的index位置开始执行,动画效果将会是从矩阵中间向四周蔓延。

image.png

5月-19-2023 09-48-29.gif

anime.stagger也可以为线性执行,如anime.stagger(60),所有元素将会按索引0开始线性执行。Value还可以为数组类型,例如rotate: anime.stagger([-360, 360]),第3个元素的旋转角度为-360 + (360 - (-360)) / 5 * 2。

image.png

可以说stagger是anime.js动画的灵魂,也是支持动画批量执行的核心能力。但stagger的实现并不复杂。

stagger API的结构如下,包含val、params两个参数,以delay: anime.stagger(60, {grid: grid, from: index})为例,val=60表示每个元素间隔60ms执行;第二个参数指定元素排列形式,例如以grid[32,16]排列,第一个元素从index位置开始。

function stagger(val, params = {}) { ...计算初始值 // 返回函数,函数体内遍历所有的元素并计算新值 return (el, i, t) => { ... return newVal + unit; } }

stagger函数整体可分为初始化、返回计算函数两部分。

首先是初始化, easing计算缓动效果,如easeInOutQuad淡入淡出。from选项指定什么位置的元素先执行,from可为first、center、last类型。stagger第一个参数可为数组类型,例如rotate: anime.stagger([-360, 360]),角度变化为-360-360=-720,第i个元素的角度变化-360 + ((360 - (-360)) / t) * i, t表示元素个数。

function stagger(val, params = {}) { const direction = params.direction || 'normal'; const easing = params.easing ? parseEasings(params.easing) : null; const grid = params.grid; const axis = params.axis; // 从第几个元素开始 let fromIndex = params.from || 0; // from可以为字符串,包含开始、中间、结束为止 const fromFirst = fromIndex === 'first'; const fromCenter = fromIndex === 'center'; const fromLast = fromIndex === 'last'; // val值是否为数组类型 const isRange = is.arr(val); const val1 = isRange ? parseFloat(val[0]) : parseFloat(val); const val2 = isRange ? parseFloat(val[1]) : 0; // 从val获取单位 const unit = getUnit(isRange ? val[1] : val) || 0; // 第一个元素动画从start开始 const start = params.start || 0 + (isRange ? val1 : 0); let values = []; let maxValue = 0; ... }

有些动画属性带有单位,如translateX: anime.stagger('.075rem', {grid: grid, from: index, axis: 'x'}), 使用getUnit可获取属性单位,支持的单位如下代码所示。start指定动画属性从start位置开始执行。

function getUnit(val) { const split = /[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec(val); if (split) return split[1]; }

其次是返回动画函数,fromIndex计算起始位置,如fromCenter为true则起始位置从中间(t - 1)/2开始。stagger使用闭包,将计算结果缓存到values中,下次执行时如果values有值则直接返回结果即可。

假设grid不为空,需要计算元素在网格grid中X、Y两个方向的位移量,fromX、fromY计算元素在X、Y两个方向的起始索引。

function stagger(val, params = {}) { ... // t表示元素数量total return (el, i, t) => { // 计算fromIndex从什么位置开始,从头、从中间、从尾部 if (fromFirst) fromIndex = 0; if (fromCenter) fromIndex = (t - 1) / 2; if (fromLast) fromIndex = t - 1; if (!values.length) { // 遍历所有元素 for (let index = 0; index < t; index++) { if (!grid) { // 如果grid为空则按队列顺序添加 values.push(Math.abs(fromIndex - index)); } else { // 如果非fromCenter,fromX从fromIndex*grid[0]位置开始;否则,从中间位置开始 const fromX = !fromCenter ? fromIndex%grid[0] : (grid[0]-1)/2; // 如果非fromCenter,fromY从fromIndex/grid[0]行开始;否则,从中间行位置开始 const fromY = !fromCenter ? Math.floor(fromIndex/grid[0]) : (grid[1]-1)/2; const toX = index%grid[0]; // 目标X方向位置 const toY = Math.floor(index/grid[0]); // 目标Y方向位置 const distanceX = fromX - toX; // X方向移动长度 const distanceY = fromY - toY; // Y方向移动长度 let value = Math.sqrt(distanceX * distanceX + distanceY * distanceY); // 移动距离 if (axis === 'x') value = -distanceX; if (axis === 'y') value = -distanceY; values.push(value); // 将结果添加到values中 } maxValue = Math.max(...values); // 移动的最长距离 } if (easing) values = values.map(val => easing(val / maxValue) * maxValue); // 添加缓动效果 if (direction === 'reverse') values = values.map(val => axis ? (val < 0) ? val * -1 : -val : Math.abs(maxValue - val)); } const spacing = isRange ? (val2 - val1) / maxValue : val1; // 单位步长 return start + (spacing * (Math.round(values[i] * 100) / 100)) + unit; // 当前元素i的移动长度 } }

所有元素的起始位置都为fromX、fromY,然后通过toX、toY设置位移量,如下图网格grid[10,9],中心位置fromX=4、fromY=4,第1、5、6行元素的目标位置[toX、toY]分别为[1, 5]、[5,8]、[6, 3]。而移动距离为from、to之间的笛卡尔坐标距离。

image.png

spacing计算单位步长,假如设置初始val为[-360,360],那么spacing=-720/maxValue,而mavValue表示中心点到到元素的最大距离,这里为到元素[9,8]的距离。最终元素i的属性移动量为start + (spacing * (Math.round(values[i] * 100) / 100)) + unit。

stagger支持对元素集合批量设置变化量,支持网格grid或者队列排列模式,位移方向也支持按axisX、axisY单独设置, 也就是说一个元素默认可以沿着两个方向同时位移,也可以单独沿着X或Y方向位移。

keyFrames

参数设置、事件机制、stagger为动画的基础,而keyFrames关键帧才是动画执行最核心的模块。

animation = anime({ ... keyframes: [ { zIndex: function(el, i, total) { return Math.round(anime.stagger([numberOfElements, 0], {grid: grid, from: index})(el, i, total)); }, translateX: anime.stagger('-.001rem', {grid: grid, from: index, axis: 'x'}), translateY: anime.stagger('-.001rem', {grid: grid, from: index, axis: 'y'}), duration: 200 }, { translateX: anime.stagger('.075rem', {grid: grid, from: index, axis: 'x'}), translateY: anime.stagger('.075rem', {grid: grid, from: index, axis: 'y'}), scale: anime.stagger([2, .2], {grid: grid, from: index}), backgroundColor: staggeredGridColors, duration: 450 }, { translateX: 0, translateY: 0, scale: 1, duration: 500, } ], }, 30)

上述代码的keyFrames共包含三项, 第一项执行200ms,第二项执行450毫秒,第三项执行500ms。可以看下每一项的动画效果:

帧二:假如起始index从0开始,则由近到远元素逐渐变小,并且颜色由浅变深。

6月-01-2023 00-48-23.gif

帧三:从第0个元素开始,由近到远依次恢复元素的大小和位置。

6月-01-2023 00-53-43.gif

元素一共有32*16=512个,以上的keyFrames为输入时格式,在最终执行动画之前会经过一系列转换: 原始输入->属性扁平化->获取元素->动画绑定->动画执行,以上的逻辑包含在createNewInstance函数中。

function createNewInstance(params) { const instanceSettings = replaceObjectProps(defaultInstanceSettings, params); const tweenSettings = replaceObjectProps(defaultTweenSettings, params); const properties = getProperties(tweenSettings, params); const animatables = getAnimatables(params.targets); const animations = getAnimations(animatables, properties); const timings = getInstanceTimings(animations, tweenSettings); const id = instanceID; instanceID++; return mergeObjects(instanceSettings, { id: id, children: [], animatables: animatables, animations: animations, duration: timings.duration, delay: timings.delay, endDelay: timings.endDelay }); } 原始输入: 即keyFrames参数 属性扁平化: replaceObjectProps(defaultTweenSettings, params)初始化缓动动画参数,包含delay、duration、easing、endDelay属性。

image.png

getProperties(tweenSettings, params)方法内部会调用flattenKeyframes将输入的keyFrames参数扁平化处理,处理后的格式如下,将原keyFrames中设置的zIndex、translateX等5个属性提取为数组形式,每一项包含{name, tweens},tweens为关键帧动画参数。

image.png

获取元素: 获取需要执行动画的所有元素, 调用getAnimatables(params.targets)方法将dom元素格式化为{ id, target, i, total, transforms},id为元素索引,target为DOM实体,i为索引,total为元素总数,transfroms保存元素的原始transforms属性并提取为数组格式。执行结果如下图,一共包含512个元素。

image.png

动画绑定:属性扁平化、获取元素两个阶段之后,需要将元素和扁平化的动画结合起来,这个过程在getAnimations(animatables, properties)方法中执行,参数animatables为元素列表,properties为扁平化后的属性。getAnimations方法比较简单,为512个元素animatables分别创建属性动画,512个元素、5个属性一共生成2560个动画animation。 function getAnimations(animatables, properties) { return filterArray(flattenArray(animatables.map(animatable => { return properties.map(prop => { return createAnimation(animatable, prop); }); })), a => !is.und(a)); }

每一项animation包含的属性有animatable、delay、duration、endDelay、property、tweens,animatable为DOM元素,tweens为缓动动画列表。

image.png

动画执行:动画执行也是anime.js最核心、最复杂的一部分,在下一节中详细介绍。 动画执行

动画实体instance包含play方法,用于启动动画执行,每一个实体会push到activeInstances队列中,然后在engine方法中执行。

instance.play = function() { if (!instance.paused) return; if (instance.completed) instance.reset(); instance.paused = false; activeInstances.push(instance); resetTime(); engine(); }

engine方法调用requestAnimationFrame(step),在每一帧执行step函数,而step函数会遍历上述提到的activeInstances列表,如果动画实体已暂停paused为true,则从列表中移除。否则,调用activeInstance.tick执行动画。

const engine = (() => { let raf; function play() { if (!raf && (!isDocumentHidden() || !anime.suspendWhenDocumentHidden) && activeInstances.length > 0) { raf = requestAnimationFrame(step); } } function step(t) { // memo on algorithm issue: // dangerous iteration over mutable `activeInstances` // (that collection may be updated from within callbacks of `tick`-ed animation instances) let activeInstancesLength = activeInstances.length; let i = 0; while (i < activeInstancesLength) { const activeInstance = activeInstances[i]; if (!activeInstance.paused) { activeInstance.tick(t); i++; } else { activeInstances.splice(i, 1); activeInstancesLength--; } } raf = i > 0 ? requestAnimationFrame(step) : undefined; } ...

tick会在每一帧执行,其内部会调用前面已经介绍过的setInstanceProgress更新动画进度的方法。

instance.tick = function(t) { now = t; if (!startTime) startTime = now; setInstanceProgress((now + (lastTime - startTime)) * anime.speed); }

setInstanceProgress在触发begin、change、complete等各个事件的同时调用setAnimationsProgress执行具体的动画效果。

在介绍setAnimationsProgress方法之前,先回顾下每一个DOM元素生成的动画对象,如下图所示,包含animatable、delay、duration、endDelay、property属性,另外还包含缓动动画列表tweens。

image.png tweens列表中每一项都包含start、delay、duration、end、endDelay,时间顺序如下图, 而属性值的变化区间[from, to]。

image.png

了解清楚动画属性,接下来查看setAnimationProgress方法内部逻辑, 方法首先遍历animations动画列表,一个元素的缓动动画tweens虽然为列表,但命中当前时刻的关键帧肯定只有一项,通过t => insTime < t.end找到目前时间的第一项缓动动画。对于一个动画,已执行的时间段赋值到eased(范围[0, 1]),一个关键帧属性变化范围从from.numbers变至to.numbers,那么当前最新的属性值即为value = fromNumber + (eased * (toNumber - fromNumber))。最后的strings列表遍历,目的是为变化值加上对应的单位,如0.5rem。

function setAnimationsProgress(insTime) { let i = 0; const animations = instance.animations; const animationsLength = animations.length; // 遍历所有的动画实体 while (i < animationsLength) { const anim = animations[i]; // DOM元素 const animatable = anim.animatable; // 每个元素对应的缓动动画列表 const tweens = anim.tweens; const tweenLength = tweens.length - 1; let tween = tweens[tweenLength]; // 由于关键帧keyframes列表中同时只会执行一个,因此使用insTime (insTime < t.end))[0] || tween; // 已执行时间段,范围[0, 1] const elapsed = minMax(insTime - tween.start - tween.delay, 0, tween.duration) / tween.duration; // 缓动动画后的已执行时间段 const eased = isNaN(elapsed) ? 1 : tween.easing(elapsed); const strings = tween.to.strings; const numbers = []; const toNumbersLength = tween.to.numbers.length; let progress; for (let n = 0; n < toNumbersLength; n++) { let value; const toNumber = tween.to.numbers[n]; const fromNumber = tween.from.numbers[n] || 0; value = fromNumber + (eased * (toNumber - fromNumber)); numbers.push(value); } // Manual Array.reduce for better performances const stringsLength = strings.length; progress = strings[0]; for (let s = 0; s < stringsLength; s++) { const a = strings[s]; const b = strings[s + 1]; const n = numbers[s]; if (!isNaN(n)) { if (!b) { progress += n + ' '; } else { progress += n + b; } } } setProgressValue[anim.type](animatable.target, anim.property, progress, animatable.transforms); anim.currentValue = progress; i++; } }

要将计算后的最新属性值progress绑定到DOM要素的style上,还需调用setProgressValue方法来执行,例如setProgressValue['transform'](dom, 'translateX', '0.5rem', transforms),transforms是什么?假如dom当前的style.transform = 'scale(2)',那么这里的transforms格式为: { list: ['scale(2)'] },同时会将新专递的translateX附加到list,那么新的transforms为{ list: ['scale(2)', 'translateX('0.5rem')'] }, 最终将完成的transorms转换字符串并赋值给dom.style.transform。

const setProgressValue = { transform: (t, p, v, transforms, manual) => { transforms.list.set(p, v); if (p === transforms.last || manual) { let str = ''; transforms.list.forEach((value, prop) => { str += `${prop}(${value}) `; }); t.style.transform = str; } } }

执行动画的调用链路总结为play->step->tick->setAnimationsProgress->setProgressValue。

总结

由于提供了stagger特性,anime.js可支持矩阵元素的动画效果。在执行周期,可通过delay、duration、endDelay等属性设置元素与元素之间的动画间隔。类似于CSS的关键帧,anime.js也提供了keyFrames关键帧列表,支持元素按关键帧列表序列依次执行动画。

除了以上的特性,anime.js还提供有timeline、controls、svg等特性。

timeline: 可设置多个动画按时间轴顺序依次执行 // Create a timeline with default parameters var tl = anime.timeline({ easing: 'easeOutExpo', duration: 750 }); // Add children tl .add({ targets: '.basic-timeline-demo .el.square', translateX: 250, }) .add({ targets: '.basic-timeline-demo .el.circle', translateX: 250, }) .add({ targets: '.basic-timeline-demo .el.triangle', translateX: 250, }); controls: 支持动画控制,如暂停、重置、反向等操作。

image.png

SVG: 获取svg元素的path路径,沿着path执行动画 var path = anime.path('.motion-path-demo path'); anime({ targets: '.motion-path-demo .el', translateX: path('x'), translateY: path('y'), rotate: path('angle'), easing: 'linear', duration: 2000, loop: true });

写在最后,如果大家有疑问可直接留言,一起探讨!感兴趣的可以点一波关注。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有